/*
 * RedMan.java
 *
 * Created on May 2, 2002, 1:48 PM
 */

package com.xtera.nuwave.tools.redundancy.redman;

import java.lang.*;
import java.util.*;
import java.io.*;
import java.net.*;
import java.text.*;
import javax.net.*;
import javax.net.ssl.*;
import com.sun.net.ssl.*;
import java.rmi.server.*;
import java.security.KeyStore;
import javax.security.cert.X509Certificate;
import org.apache.log4j.*;


/**
 *
 * @author  llaranje
 * @version 
 */
public class RedMan {
    
    public static Object restartsem = new Object();

    public static Properties cfgtable = null;
    public static Object sem = new Object(); 
    public static int port = 0;
    public static int order = 0;
    public static Process backup = null;
    public static PrintWriter backupwriter = null;

    public static String cfgfilepath = null;
    public static String cfgfiledir = null;
    public static String username = null;
    static final boolean server = true;
    static final boolean client = false;
    static boolean primary = true;
    static boolean protection_on = false;
    static boolean agent;
    static String role = null;
    static boolean bootstart = true;
    static int hbnum = 0;
    static int appnum = 0;
    static int mypid = 0;
    static int myprimarypid = 0;
    static int mybackuppid = 0;
    static final int MINHBSLOTS = 10;
    static Heartbeat[] hbs;
    static Heartbeat hbself = null;
    static AppMonitor[] appmons; 
    static FileInputStream cfgfile = null;
    static SSLServerSocketFactory sslSrvFactory = null;
    static SSLServerSocket svso = null;
    
    private static final String CFGFILENAME = "RedMan.conf";
    private static final String REDMANCLASS = "com.xtera.nuwave.tools.redundancy.redman.RedMan";
    private static final boolean FULL = true;
    private static final boolean COMPLEMENT = false;
    
    private static Log4RedManEven logstartEven = null;
    private static Log4RedManOdd logstartOdd = null;
    private static Category trace = null;
    
    /**
    * @param args the command line arguments
    */
    public static void main (String args[]) throws UnknownHostException, FileNotFoundException {
        System.err.println("RedMan running ...");  
        
        // Set JVM timezone to GMT
        TimeZone.setDefault(new SimpleTimeZone(0, "GMT"));       
        
        if (handleParameters(args)) {
            String ldres = loadConfigFile(cfgfilepath);
            if (ldres.equals("OK")) {
                if (primary) {
                    // RedMan instance is primary
                    try {
                        holdonpids = true;
                        trace.info("Will call primaryBehavior()");
                        setupHeartbeats();
                        primaryBehavior();

                        trace.info("RedMan: will call getMySSLServerSocketFactory()");
                        getMySSLServerSocketFactory();
                        trace.info("RedMan: executed getMySSLServerSocketFactory()");
                        
                        // Create SSL server socket
                        trace.info("RedMan (primary): will call createSSLServerSocket()");
                        svso = createSSLServerSocket(port);
                        trace.info("RedMan (primary): executed getSSLServerSocket()");
                        if (setupMonitoring(FULL)) {
                            //trace.debug("Will change to Real-Time priority");
                            launchConnectionManager();
                        } 
                    }
                    catch (Exception e) {
                        if (trace != null) trace.error("Failure in RedMan() (primary): " + e.getMessage());
                    }
                }
                else {                   
                    try {
                        // Write pid to standard output to be read by the primary RedMan
                        trace.debug("Backup RedMan will write its pid=" 
                                                    + String.valueOf(mypid) + " to standard output");
                        System.out.println("BKPPID " + String.valueOf(mypid));
                        System.out.flush();
                                                
                        // RedMan instance is backup, waits until primary fails
                        backupBehavior();
                        
                        // RedMan became primary. 
                        // Kill old primary just in case it is a hang or timing problem
                        CompMon.killPid(myprimarypid);
                        primary = true;
                        
                        // Start heartbeats
                        setupHeartbeats();
                    
                        // RedMan instance is now primary
                        primaryBehavior();
                        
                        // Get server socket factory 
                        trace.info("RedMan (new primary): will call getMySSLServerSocketFactory()");
                        getMySSLServerSocketFactory();
                        
                        // Create server socket
                        trace.info("RedMan (new primary): will call createSSLServerSocket()");
                        svso = createSSLServerSocket(port);

                        // Start monitoring applications
                        trace.debug("New Primary will call setupMonitoring()");
                        if (setupMonitoring(COMPLEMENT)) {
                            trace.debug("New Primary will call launchConnectionManager()");
                            launchConnectionManager();
                        }
                    }
                    catch (Exception e) {
                        if (trace != null) trace.error("Failure in RedMan() (new primary): " + e.getMessage());
                    }
                }           
            }
            else {
                System.out.print(ldres);
                if (trace != null) trace.error(ldres);
            }
        }
        if (trace != null) trace.error("Failed RedMan exiting without starting the backup ...");
        System.out.println("Failed RedMan exiting without starting the backup ...");
        System.out.println();
        System.exit(0);
    }
    
    private static boolean handleParameters(String args[]) {
        // Check if this RedMan instance is primary or backup
        if ((args.length > 0) && (args[0].startsWith("-b"))) {
            primary = false;
            bootstart = false;
            order = Integer.parseInt(args[0].substring(2));
        }       

        if (args.length > 2) {
            System.out.println("USAGE: java <package-name>.RedMan <config-file-dir>");
            return(false);
        }        
        if (args.length == 0) {
            cfgfilepath = CFGFILENAME;
        }
        if (args.length == 1) {
            if (!args[0].startsWith("-b")) {
                cfgfiledir = args[0];
                cfgfilepath = cfgfiledir + "/" + CFGFILENAME;
            }
            else {
                cfgfilepath = CFGFILENAME;
            }
        }         
        if (args.length == 2) {
            if (!args[0].startsWith("-b")) {
                System.out.println("USAGE: java <package-name>.RedMan <config-file-dir>");
                return(false);
            }
            cfgfiledir = args[1];
            cfgfilepath = cfgfiledir + "/" + CFGFILENAME;
        }
        
        // Initialize log4j
        if ((order % 2) == 0) logstartEven = new Log4RedManEven();
        else logstartOdd = new Log4RedManOdd();
        trace = Category.getInstance(RedMan.class);
        
        // Get certain properties
        getMyPid();
        username = System.getProperty("user.name");
        
        if (primary) {
            trace.info("\n\n\n-------------------- PRIMARY REDMAN ["
                                    + order + "] STARTED (User = " + username + ", Pid = " 
                                    + Integer.toString(mypid) + ") --------------------\n");
        }
        else {
            trace.info("\n\n\n-------------------- BACKUP REDMAN ["
                                    + order + "] STARTED (User = " + username + ", Pid = " 
                                    + Integer.toString(mypid) + ") --------------------\n");
        }
        return(true);
    }
    
    static boolean checkConfigFile(String filepath) {
        try {
            cfgfile = new FileInputStream(filepath);           
            return(true);
        }
        catch (FileNotFoundException e) {
            trace.error("Config file not found");
            return(false);
        }        
    }
    
    static String loadConfigFile(String filepath) {
        if (!checkConfigFile(filepath)) return("Unable to open config file. ");
        try {
            cfgtable = new Properties();
            cfgtable.load(cfgfile);
        }
        catch (IOException e) {
            trace.error("Unable to open/load config file " + cfgfilepath 
                                                           + ": IOException,  " + e.getMessage());
            return("Unable to open/load config file. ");
        }
        
        // Check if RedMan is protected and set protection flag if so
        String rmprot = cfgtable.getProperty("REDMAN_PROTECTION");
        if (rmprot != null) {
            rmprot = rmprot.trim();
            if (rmprot.equalsIgnoreCase("ON")) protection_on = true;
        }
        
        return("OK");
    }
    

    static void createHeartbeats() throws UnknownHostException {
        String hblist = cfgtable.getProperty("HEARTBEATS");
        if (hblist == null) {
            trace.warn("No heartbeats defined for RedMan");
            hbs = new Heartbeat[MINHBSLOTS];
            return;
        }
        trace.info("HEARTBEATS = " + hblist);

        StringTokenizer st = new StringTokenizer(hblist);
        hbnum = st.countTokens();
        hbs = new Heartbeat[hbnum+MINHBSLOTS];
        
        int i = 0;
        while (st.hasMoreTokens()) {
            String hbname = st.nextToken();
            hbs[i] = new Heartbeat(hbname);
            i++;
        }
    }
    
    static void startHeartbeats() throws UnknownHostException {
        for (int i=0; i < hbnum; i++) {
            trace.info("Starting heartbeat " + hbs[i].name);
            hbs[i].startHeartbeat();
        }
    }
   
    static void setupApplicationMonitors() throws UnknownHostException {
        String applist = cfgtable.getProperty("APPLICATIONS");
        trace.info("APPLICATIONS = " + applist);
        StringTokenizer st = new StringTokenizer(applist);
        appnum = st.countTokens();
        appmons = new AppMonitor[appnum];
        
        int i = 0;
        while (st.hasMoreTokens()) {
            String appname = st.nextToken();
            appmons[i] = new AppMonitor(appname, cfgtable, false);  
            i++;
        }
    }
    
    static void createApplicationMonitors() throws UnknownHostException {
        String applist = cfgtable.getProperty("APPLICATIONS");
        trace.debug("APPLICATIONS = " + applist);
        StringTokenizer st = new StringTokenizer(applist);
        appnum = st.countTokens();
        appmons = new AppMonitor[appnum];
        
        int i = 0;
        while (st.hasMoreTokens()) {
            String appname = st.nextToken();
            
            trace.debug("CreateApplicationMonitors() " + ": primary = " + primary);

            appmons[i] = new AppMonitor(appname, false);  
            i++;
        }
    }
    
    static void activateApplicationMonitors() throws UnknownHostException {        
        for (int i = 0; i < appnum; i++) {
            trace.debug("Will activate application monitor: " + appmons[i].name);
            appmons[i].getGoing();
        }
    }
    
    static void setupHeartbeats() {
        try { 
            // Create the primary HBSELF heartbeat
            if (protection_on) {
                hbself = new Heartbeat(true);
                hbself.startHeartbeat();
            }
            
            // Create and start the application heartbeats
            createHeartbeats();
            startHeartbeats();
        }
        catch (Exception e) {
            trace.error("Exception setting up heartbeats: " + e.getMessage());
        } 
    }
    
    static boolean setupMonitoring(boolean full)  {
        try {
            if (full) setupApplicationMonitors();
            else activateApplicationMonitors();
        }
        catch (Exception e) {
            trace.error("Exception: " + e.getMessage());
            return(false);
        }
        return(true);
    }
    
    static void sendPidsToNewBackupRedMan() {
        for (int i = 0; i < appnum; i++) {
            trace.debug("Will send pids for app " + appmons[i].name + " to the new backup RedMan");
            appmons[i].sendAppPidsToBackupRedMan();
        }   
    }
    
    static void launchConnectionManager() {
        try {
            while (true) {
                ConnManager hacm = new ConnManager();
                Thread.sleep(3000);
            }
        }
        catch (Exception e) {
            trace.error("Exception: " + e.getMessage());
            System.exit(1);
        }
    }
    
    static private boolean isDown() {
        StringTokenizer st = new StringTokenizer(hbself.event);
        String host = st.nextToken();
        String ev = st.nextToken();
        trace.debug("HBSELF event received = \'" + ev + "'");
        
        if (ev.equals("DOWN")) {
            if (bootstart) trace.info("BACKUP RedMan is DOWN.");
            else trace.info("PRIMARY RedMan is DOWN");
            return(true);
        }
        else if (ev.equals("UP")) {
            if (bootstart) trace.info("BACKUP RedMan is UP.");
            else trace.info("PRIMARY RedMan is UP");
            return(false);
        }
        
        return(true);
    }
    
    static boolean firsttime = true;
    
    private static void waitForBackupToFail() {
        try {
            while (true) {
                trace.debug("PRIMARY RedMan: will synchronize on semaphore");
                synchronized (hbself.sem) {                   
                    if (firsttime) {
                        //trace.debug("PRIMARY RedMan: will set HBSELF running");
                        //hbself.startHeartbeat();
                        firsttime = false;
                    }
                    trace.debug("PRIMARY RedMan: will wait for event");
                    hbself.sem.wait();
                    trace.debug("PRIMARY RedMan: received event");
                    if (isDown()) break;
                }
            }                   
        }
        catch (Exception e) {
            trace.error("Exception: " + e.getMessage());
            System.exit(1);
        }        
    }
    
    private static class CreateBackup extends Thread {
        public CreateBackup() {
            setPriority(Thread.MAX_PRIORITY - 1);
            trace.debug("Task primaryTask (create backup) was created and its priority set to MAX_PRIORITY-1");            
            start();
        }
        
        public void run() {
            String cmd = System.getProperty("java.exec") + " -cp " + System.getProperty("java.class.path")
                                  + " -Djava.exec=" + System.getProperty("java.exec") + " -Xincgc "
                                  + " -Dutildir=" + System.getProperty("utildir") + " "
                                  + REDMANCLASS + " -b" + String.valueOf(order+1);
            if (!cfgfiledir.equals(null)) cmd = cmd + " " + cfgfiledir;
            trace.debug("Primary RedMan will create backup RedMan: \"" + cmd + "\"");

            try {
                do {
                    // Start backup RedMan
                    backup = Runtime.getRuntime().exec(cmd);
                    trace.info("Primary Redman: created backup RedMan");
                
                    // Send pid to backup RedMan
                    sendMyPidToBackupRM();
                
                    // Read pid from backup RedMan
                    readBackupRMPid();
                
                    // Send pids to the new backup
                    if (holdonpids) holdonpids = false;
                    else sendPidsToNewBackupRedMan();

                    // Wait for backup to fail
                    waitForBackupToFail();
                
                    // Kill old backup just in case it is a timing problem
                    CompMon.killPid(mybackuppid);
                    mybackuppid = 0;
                } while(true);
            }
            catch (IOException e) {
                trace.error("Exception creating backup RedMan: " + e.getMessage());
                System.exit(1);
            }
        }
    
        private static void readBackupRMPid() {
            try {
                // Obtain process pid from backup RedMan
                InputStream pin = backup.getInputStream();
                InputStreamReader cin = new InputStreamReader(pin);
                BufferedReader in = new BufferedReader(cin);
                String inputst;
                do { 
                    inputst = in.readLine();
                } while ((inputst == null) || !inputst.startsWith("BKPPID"));
        
                StringTokenizer st = new StringTokenizer(inputst); st.nextToken();
                String stpid = st.nextToken();
        
                trace.info("Got backup RedMan pid = [" + stpid + "]");
                mybackuppid = Integer.parseInt(stpid);               
                in.close();
            }
            catch (Exception e) {
                trace.error("Exception reading backup RedMan PID: " + e.getMessage());
            }
        }
    
        private static void sendMyPidToBackupRM() {
            // Create the output stream to the backup RedMan
            OutputStream bout = backup.getOutputStream();
            backupwriter = new PrintWriter(new OutputStreamWriter(bout));
            trace.info("Primary Redman: created output stream to backup RedMan");
                
            // Send pid to backup RedMan
            backupwriter.println(String.valueOf(mypid));
            backupwriter.flush();
            trace.info("Sent my pid (" + String.valueOf(mypid) + ") to backup RedMan");        
        }
    }
    
    static boolean holdonpids;
    
    private static void primaryBehavior() throws UnknownHostException, IOException {
        String stport = cfgtable.getProperty("REDMAN_PORT");
        if (stport != null) {
            stport = stport.trim();
            port = Integer.parseInt(stport);
        }
        else {
            trace.error("No port defined for Redman");
        }
        
        // Check if backup RedMan should be created.
        // If so, instantiate task that creates the backup and waits on it
        if (protection_on) primaryTask = new CreateBackup();
        else trace.debug("RedMan not protected (backup RedMan will not be started)");
                        
        trace.debug("Ending execution of primaryBehavior()");
    }
     
    private static CreateBackup primaryTask = null;
    private static ReceiveMsgsFromPrimaryRedMan backupTask = null;
    
    private static void waitForPrimaryToFail() {
        boolean firsttime = true;
        try {
            while (true) {
                trace.debug("BACKUP RedMan: will synchronize on semaphore");
                synchronized (hbself.sem) {
                    trace.debug("BACKUP RedMan: will wait for event");
                    if (firsttime) {
                        //trace.debug("BACKUP RedMan: will set HBSELF running");
                        //hbself.startHeartbeat();
                        firsttime = false;
                    }                    
                    hbself.sem.wait();
                    trace.debug("BACKUP RedMan: received event");
                }    
                if (isDown()) break;
            }
        }
        catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }        
    }   
    
    private static class ReceiveMsgsFromPrimaryRedMan extends Thread {
        public ReceiveMsgsFromPrimaryRedMan() {
            setPriority(Thread.MAX_PRIORITY - 1);
            trace.debug("Task backupTask was created and its priority set to MAX_PRIORITY-1");            
            start();            
        }
    
        public void run() {
            StringTokenizer st = null;
            String thename = null, state = null, token = null, compname = null;
            AppMonitor appmon = null;
        
            trace.debug("BACKUP RedMan entered method receiveStateInfo()");
        
            BufferedReader fromprimary = new BufferedReader(new InputStreamReader(System.in));
        
            trace.debug("BACKUP RedMan created BufferedReader to receive state info from PRIMARY RedMan");
        
            try {
                token = fromprimary.readLine();
                myprimarypid = Integer.parseInt(token);
                trace.debug("Backup RedMan got pid from primary RedMan, pid = " + token);
            }
            catch (IOException e) {
                trace.error("Backup RedMan could not read pid from primay RedMan - " + e.getMessage());
            }
               
            try {
                // Loop receiving state info and pid messages from the Primary RedMan
                for (String msg = null; (msg = fromprimary.readLine()) != null;) {
                    if (msg.length() == 0) break;
                    msg = msg.trim();

                    // Print message to log file
                    trace.debug("BACKUP RedMan received msg: \"" + msg + "\" from PRIMARY RedMan");

                    // Get the destination name for this message
                    st = new StringTokenizer(msg);
                    thename = st.nextToken();
                
                    // Get next token (the message type)
                    token = st.nextToken();

                    if (!thename.equals("REDMAN")) {
                        // Find the application monitor
                        for (int i = 0; i < appnum; i++) {
                            if (appmons[i].name.equals(thename)) {
                                appmon = appmons[i];
                                break;
                            }
                        }
                               
                        // Process message
                        if (token.equals("STATE")) {
                            // Get state and assign it to the app initstate
                            state = st.nextToken();
                            appmon.initstate = state;
                        }
                        if (token.equals("PID")) {
                            // Get the component name
                            compname = st.nextToken();
                            // Get the string of pids
                            String pidstring = " ";
                            while (st.hasMoreTokens()) {
                                pidstring += st.nextToken();
                            }
                            appmon.assignComponentPids(compname, pidstring);
                        }
                    }
                    else {       // This should be a shutdown command for the backup RedMan
                        if (token.equals("SHUTDOWN")) {
                            trace.debug("Backup RedMan exiting because of a SHUTDOWN command");
                            System.exit(0);
                        }
                    }
                }
            }
            catch (Exception e) {
                trace.error("BACKUP RedMan Exception: " + e.getMessage());         
            }            
        }
    }

    private static void backupBehavior() throws UnknownHostException, FileNotFoundException {          
        // Create application monitor objects but do not let the 
        // executing threads be started
        createApplicationMonitors();
        
        // Create thread to receive state information from the primary RedMan
        backupTask = new ReceiveMsgsFromPrimaryRedMan();
        
        hbself = new Heartbeat(false);
        hbself.startHeartbeat();
        waitForPrimaryToFail();
        
        trace.info("\n\n *** REDMAN (" + order + ") BECAME PRIMARY *** \n");
    }
     
    static private void getMySSLServerSocketFactory() throws IOException {
        char[] passphrase = "haleydog".toCharArray();        
        try {
            trace.info("Will initialize provider to obtain socket factory");
            System.setProperty("java.protocol.handler.pkgs",
                                           "com.sun.net.ssl.internal.www.protocol");
            java.security.Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
            
            // Set up key manager to do server authentication
            trace.info("Will set up key manager to do server authentication");
            com.sun.net.ssl.SSLContext ctx = com.sun.net.ssl.SSLContext.getInstance("TLS");
            trace.info("Got SSLContext");
            com.sun.net.ssl.KeyManagerFactory kmf = 
                       com.sun.net.ssl.KeyManagerFactory.getInstance("SunX509");
            KeyStore ks = KeyStore.getInstance("JKS");

            String keysFile = "/com/xtera/nuwave/interfaces/ssl/jssecacerts";

            InputStream istream = RedMan.class.getResourceAsStream( keysFile );

            ks.load( istream, passphrase);
            kmf.init(ks, passphrase);
            ctx.init(kmf.getKeyManagers(), null, null);

            // Get the server socket factory
            trace.info("Will obtain the SSL server socket factory");
            sslSrvFactory = (SSLServerSocketFactory) ctx.getServerSocketFactory();
            trace.info("Obtained the SSL server socket factory");  
        }
        catch (Exception e) {
            trace.error("Exception: " + e.getMessage());
        }
    }
 
    static private void getMyPid() {
        try {
            String utildir = System.getProperty("utildir");
            String cmd = utildir + "/getppid";
            Process p = Runtime.getRuntime().exec(cmd);   
        
            // Obtain process pid from starting script
            InputStream pin = p.getInputStream();
            InputStreamReader cin = new InputStreamReader(pin);
            BufferedReader in = new BufferedReader(cin);
            String stpid = in.readLine();
            mypid = Integer.parseInt(stpid);
            //trace.info("Got RedMan pid = " + stpid);
            in.close();
        }
        catch (IOException e) {
            trace.error("Exception reading mypid - " + e.getMessage());
        }
    }
    
    static private SSLServerSocket createSSLServerSocket(int port) throws IOException {
        SSLServerSocket svso = null;
        try {            
            // Create the SSL server socket
            trace.info("Will create SSL server socket to listen to port " + String.valueOf(port));
            svso = (SSLServerSocket) sslSrvFactory.createServerSocket(port);
            trace.info("Created SSL server socket");
            
            return(svso);
        }
        catch (Exception e) {
            trace.error("Exception: " + e.getMessage());
            if (svso != null) svso.close();
        }
        return(svso);
    }
    
    static void makePriorityTS() {
        if (mypid > 0) {
            String cmd = "/bin/priocntl -s -c TS -i pid " + Integer.toString(mypid);
            try {
                Runtime.getRuntime().exec(cmd);
                trace.debug("RedMan's priority changed to Time-Share");
            }
            catch (Exception e) {
                trace.warn("Could not change RedMan's priority to Time-Share - " + e.getMessage());
            }
        }
        else trace.warn("Could not touch RedMan's priority, PID = " + Integer.toString(mypid));   
    }
    
    static void makePriorityRT() {
        if (mypid > 0) {
            String cmd = "/bin/priocntl -s -c RT -p 10 -t 20 -i pid " + Integer.toString(mypid);
            try {
                Runtime.getRuntime().exec(cmd);
                trace.debug("RedMan's priority changed to Real-Time");
            }
            catch (Exception e) {
                trace.warn("Could not change RedMan priority to Real-Time - " + e.getMessage());
            }
        }
        else trace.warn("Could not touch RedMan's priority, PID = " + Integer.toString(mypid));
    }
}
